﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security;
using System.Security.Cryptography;
using System.Text;
using System.Threading;

namespace Jack.SequentialGuid
{
	/// <summary>
	/// 
	/// </summary>
	/// <typeparam name="T"></typeparam>
	public abstract class SequentialGuidGeneratorBase<T> where T : SequentialGuidGeneratorBase<T>
	{
		private static readonly Lazy<T> Lazy =
		new Lazy<T>(() => (Activator.CreateInstance(typeof(T), true) as T)!);

		private readonly byte[] _machinePid;
		private int _increment;

		/// <summary>
		/// 
		/// </summary>
		protected SequentialGuidGeneratorBase()
		{
			_increment =
#if NETFRAMEWORK || NETSTANDARD2_0
			// Fall back to the old Random class for compatibility
			new Random().Next
#else
				// Use the RandomNumberGenerator static function where available
				RandomNumberGenerator.GetInt32
#endif
				(500000);
			_machinePid = new byte[5];
#if NET6_0_OR_GREATER
			// For newer frameworks use the preferred static function
			var hash = SHA512.HashData
#else
			// For older frameworks use the old algorithm create function
			using var algorithm = SHA512.Create();
			var hash = algorithm.ComputeHash
#endif
			(Encoding.UTF8.GetBytes(Environment.MachineName));
			for (var i = 0; i < 3; i++)
				_machinePid[i] = hash[i];
			try
			{
				var pid =
#if NET6_0_OR_GREATER
				// For newer frameworks prefer to use the static property on the Environment
				Environment.ProcessId
#else
					// For older frameworks get the process id the old school way
					Process.GetCurrentProcess().Id
#endif
				;
				// use low order two bytes only
				_machinePid[3] = (byte)(pid >> 8);
				_machinePid[4] = (byte)pid;
			}
			catch (SecurityException)
			{
			}
		}

		/// <summary>
		/// 
		/// </summary>
#pragma warning disable CA1000 // 不要在泛型类型中声明静态成员
		public static T Instance => Lazy.Value;
#pragma warning restore CA1000 // 不要在泛型类型中声明静态成员

		/// <summary>
		/// 
		/// </summary>
		/// <returns></returns>
		public Guid NewGuid() => NewGuid(DateTime.UtcNow.Ticks);

		/// <summary>
		/// 
		/// </summary>
		/// <param name="timestamp"></param>
		/// <returns></returns>
		/// <exception cref="ArgumentException"></exception>
		public Guid NewGuid(DateTime timestamp)
		{
			var ticks = timestamp.Kind switch
			{
				DateTimeKind.Utc => timestamp.Ticks, // use ticks as is
				DateTimeKind.Local => timestamp.ToUniversalTime().Ticks, // convert to UTC
				_ => throw new ArgumentException("DateTimeKind.Unspecified not supported", nameof(timestamp))
			};

			// run validation after tick conversion
			if (!ticks.IsDateTime())
				throw new ArgumentException("Timestamp must be between January 1st, 1970 UTC and now",
					nameof(timestamp));

			// Once we've gotten here we have a valid UTC tick count so yield the Guid
			return NewGuid(ticks);
		}

		internal virtual Guid NewGuid(long timestamp)
		{
			// only use low order 3 bytes
			var increment = Interlocked.Increment(ref _increment) & 0x00ffffff;
			return new Guid(
				(int)(timestamp >> 32),
				(short)(timestamp >> 16),
				(short)timestamp,
				_machinePid.Concat(
					new[] { (byte)(increment >> 16), (byte)(increment >> 8), (byte)increment }).ToArray()
			);
		}
	}
}
